Link to this headingRC4

  • Do not use
  • Has biases for the first 256 bytes output information.
  • It also has biases for all output information.

Used in WEP, TKIP, BitTorrent protocol encryption

Link to this headingSecurity

  • The Key Scheduling Algorithm is bad

Link to this headingKey Reuse

you can’t re-use the same RC4 keystream to encrypt two different messages

Link to this headingBit Flip Attacks

Padding Oracle

Link to this headingBit Bias

Here shows the bias for the first 256 bytes

PoC:

from Crypto.Cipher import ARC4 import secrets import numpy as np import pandas as pd import matplotlib.pyplot as plt try: from caas_jupyter_tools import display_dataframe_to_user HAS_DISPLAY_DF = True except Exception: HAS_DISPLAY_DF = False def rc4_keystream(key: bytes, nbytes: int): cipher = ARC4.new(key) return cipher.encrypt(b"\x00" * nbytes) TRIALS = 100000000 # PyCryptodome ARC4 is faster, so we can push higher trials NUM_POSITIONS = 16 print(f"Running RC4 bias PoC with PyCryptodome ARC4: TRIALS={TRIALS}, NUM_POSITIONS={NUM_POSITIONS} ...") counts = np.zeros((NUM_POSITIONS, 8), dtype=np.int64) for t in range(TRIALS): key = secrets.token_bytes(16) # 128-bit random key ks = rc4_keystream(key, NUM_POSITIONS) for pos in range(NUM_POSITIONS): b = ks[pos] for bit in range(8): if (b >> bit) & 1: counts[pos, bit] += 1 if (t+1) % 10000 == 0: print(f" Completed {t+1}/{TRIALS}") probs = counts / TRIALS expected = 0.5 std_err = np.sqrt(probs * (1 - probs) / TRIALS) z_scores = (probs - expected) / (std_err + 1e-30) abs_bias = np.abs(probs - expected) avg_abs_bias_per_pos = abs_bias.mean(axis=1) rows = [] for pos in range(NUM_POSITIONS): for bit in range(8): rows.append({ "position": pos+1, "bit": bit, "p_bit_is_1": probs[pos, bit], "abs_bias": abs_bias[pos, bit], "z_score": z_scores[pos, bit] }) df = pd.DataFrame(rows).sort_values(by="abs_bias", ascending=False).reset_index(drop=True) top10 = df.head(10).copy() top10.index = np.arange(1, len(top10)+1) print("\nTop 10 (position, bit) pairs by absolute bias (RC4, PyCryptodome ARC4):") print(top10[["position","bit","p_bit_is_1","abs_bias","z_score"]])

RC4 Output:

Top 10 (position, bit) pairs by absolute bias (RC4, PyCryptodome ARC4) after 100000000: position bit p_bit_is_1 abs_bias z_score 1 2 1 0.497960 0.002040 -40.791739 2 2 4 0.497979 0.002021 -40.419330 3 2 0 0.498073 0.001927 -38.546686 4 2 2 0.498077 0.001923 -38.459684 5 2 3 0.498159 0.001841 -36.821250 6 2 5 0.498211 0.001789 -35.782429 7 2 6 0.498308 0.001692 -33.836194 8 2 7 0.498596 0.001404 -28.089911 9 6 7 0.500371 0.000371 7.421202 10 7 7 0.500344 0.000344 6.878402

You can see above that certain positions and bits have up to a 0.20% Bias over 10,000,000,000

Here you can see AES to see the difference in randomness.

AES Output:

Top 10 (position, bit) pairs by absolute bias (AES-CTR, PyCryptodome) after 100000000: position bit p_bit_is_1 abs_bias z_score 1 9 6 0.499835 0.000165 -3.2936 2 15 0 0.500127 0.000127 2.5312 3 8 0 0.499884 0.000116 -2.3240 4 14 1 0.500107 0.000107 2.1382 5 30 7 0.500104 0.000104 2.0718 6 31 3 0.500102 0.000102 2.0450 7 30 0 0.499898 0.000102 -2.0382 8 6 1 0.500102 0.000102 2.0314 9 16 3 0.499900 0.000100 -2.0062 10 7 6 0.499902 0.000098 -1.9632

Link to this headingFluhrer, Mantin and Shamir attack

  • A 16-byte key can be computed from about 960 such keystreams because of the coloration of the outputs
    • The usually ways to fix this is dont use the first 3072 bytes of the output
    • Don’t concat the IV to the key. Hash them instead.

In [WEP](/Red Team/Wifi/WEP) if the IV is predictable either by a random number generator or other way it makes it easy to figure out the first byte of the keystream. Since RC4 only generates one byte at a time it makes it easy to figure out the next byte and continue down the line.

Link to this headingKlein, Dropping and Hashing

  • Reduces the operations needed to find the IV from a few million to 20,000

Link to this headingImplementation

from cryptopals_lib import * def key_schedule(input_key): key_len = len(input_key) #Just a list of 0-255 s_box = [x for x in range(256)] acc = 0 for i in range(256): # acc += s_box[i] + input_key[i % key_len] acc %= 256 #Swap the index I with the acc index s_box[acc], s_box[i] = s_box[i], s_box[acc] return s_box def pseudo_random_genreator(s_box): acc = 0 i = 0 while True: acc += 1 acc %= 256 i += s_box[acc] i %= 256 #Swap the index I with the acc index s_box[acc], s_box[i] = s_box[i], s_box[acc] index = (s_box[acc] + s_box[i]) % 256 output = s_box[index] #print(index, output) yield output def RC4(key): #Generate the s_box from the secret key s_box = key_schedule(key) #print(s_box) #Generate Keystream from s_box return pseudo_random_genreator(s_box) if __name__ == '__main__': #Test Vectors from https://tools.ietf.org/html/rfc6229 key_stream = RC4(bytes_to_intarray(b"\x01\x02\x03\x04\x05", 1)) output_data = b"" for _ in range(32): output_data += int_to_bytes(next(key_stream)) print(output_data.hex()) assert(output_data.hex() == "b2396305f03dc027ccc3524a0a1118a86982944f18fc82d589c403a47a0d0919")